# this is the payment module
# 
# this module contains the following classes:
# 
#   History - keep track of the history of payments
#   Profile - defines a template of payments

from __init__ import _ABC_AutoID_
from user import User
from util import safeFn


class Profile(_ABC_AutoID_):
    """
    Payment profile class

    Structure of a profile:
      profile
      |- id             (int, auto)
      |- name           (str)
      |- payer          (User)
      |- balance        (dict)
      |  |- payer         (User : float)
      |  |- payee #1      (User : float)
      |  |- ...
      |  +- payer #N      (User : float)
      +- history        (History)
    """

    ######## Class-level variable ########

    _profile_map_ = {
        # this maps the payment profile name back to the profile itself
        # profile name : profile object,
        }

    ######## Overridden build-in Methods / Operators ########

    def __init__(self, name, *payees):
        """ usage: Profile(name[, payer[, payee1[, payee2 ...]]]) """
        super(Profile, self).__init__()

        self.__changeName(name, new=True) # this will perform name check inside
        self.__payer = None
        self.__balance = { }
        if len(payees) > 0:
            for usr in payees:
                self.addPayee(usr)
            self.setPayer(payees[0])
        return

    def __str__(self):
        return "Profile: {0} - {1} [{2}]\n  Payer: {3}\n  Payee: {4}\n".format(
            self._id_, self.__name,
            self.isActive() and " Active " or "Inactive",
            self.getPayer(),
            self.getPayees("\n{0:9}".format(""), True)
            )
    def __repr__(self):
        return "{{'id':{0},'name':{1},'balance':{2}}}".format(
            self._id_, self.__name, self.__balance
            )


    ######## Public Functions ########

    def debug(self):
        return "[{0}] '{1}':\n{2}".format(
            self._id_, self.__name, self.printBalance(True)
            )

    @classmethod
    def getProfile(cls, name):
        if name in cls._profile_map_:
            return cls._profile_map_[name]
        return None

    def isActive(self):
        if not self.getPayer():
            return False    # no payer set               -> NOT active
        if not self.getPayer().isActive():
            return False    # payer not active           -> NOT active
        if len(self.getPayeeList(False)):
            return True     # at least 1 payee is active -> active
        return False        # only payer is active       -> NOT active

    def printBalance(self, printAll=False):
        if printAll and not self.getPayer():
            return "  *** No payer set ***"
        if not self.isActive():
            return "  *** No active users ***"
        return "  payer: {0:<16}  {1}\n  payee: {2}".format(
            self.getPayer().getName(),
            self.__balance[self.getPayer()],
            "\n{0:9}".format("").join([
                    "{0:<16}  {1}{2}".format(
                        usr.getName(),
                        self.__balance[usr],
                        not usr.isActive() and "  [Inactive]" or ""
                        )
                    for usr in self.getPayeeList(printAll)
                    ])
            )

    def calcBalance(self):
        # need implementation
        return

    def balanceCleared(self, checkUser=None):
        if isinstance(checkUser, User) and checkUser != self.getPayer():
            # check for single user
            return self.__balance[checkUser] == 0
        else:
            # check for all users
            return max([ blnc == 0
                         for ( usr, blnc ) in self.__balance.iteritems()
                         if usr.isActive() # well, all active users
                         ])


    ######## Getters / Setters ########

    def getID(self):
        return super(Profile, self).getID()

    def getName(self):
        return self.__name

    def changeName(self, name):
        self.__changeName(name, new=False)
        return


    def getPayeeList(self, getAll=False):
        return [ usr for usr in sorted(self.__balance.iterkeys())
                 if usr != self.getPayer() and (getAll or usr.isActive())
                 ]
    def getPayees(self, dlm=', ', getAll=False):
        return dlm.join([ str(usr) for usr in self.getPayeeList(getAll) ])

    def getPayer(self):
        return self.__payer

    def setPayer(self, payer):
        if payer not in self.__balance:
            raise ValueError("Payer need to be added to the profile first.")
        self.__payer = payer
        self.calcBalance()
        return

    def getPayee(self, key):
        if isinstance(key, int) or key.isdigit():
            # id lookup
            if key in [ usr.getID() for usr in self.__balance ]:
                return User.getObject(key)
        else:
            # name lookup
            usr = User.getUser(key)
            if usr in self.__balance:
                return usr
        return None             # not found

    def addPayee(self, payee):
        if payee not in self.__balance:
            self.__balance[payee] = 0 # initial balance of 0
        return

    def removePayee(self, payee):
        if payee in self.__balance:
            if payee == self.getPayer():
                if not self.balanceCleared():
                    raise ValueError("Balance need to be cleared before"
                                     " removing the payer.")
                self.__payer = None
            if not self.balanceCleared(payee):
                raise ValueError("Balance need to be cleared before removing"
                                 " payee '{0}'.".format(payee.getName()))
            del self.__balance[payee]
        return


    def getBalance(self, user):
        usr = self.getPayee(user)
        if usr is None:
            return None
        return self.__balance[usr]


    ######## Override Protected Member Functions ########

    @classmethod
    def _dump_(cls):
        # back up payer / payees
        bkup = { }
        for ( prID, profile ) in cls._obj_dict_.iteritems():
            # remove references to User instances
            bkup[prID] = [ profile.__payer,  profile.__balance ]
            if profile.__payer:
                profile.__payer = profile.__payer.getID()
            else:
                profile.__payer = ""
            profile.__balance = { } # get a new dict
            for ( usr, blnc ) in bkup[prID][1].iteritems():
                profile.__balance[usr.getID()] = blnc

        # generate dump w/o external references
        rv = super(Profile, cls)._dump_()

        # restore payer / payees
        for ( prID, profile ) in cls._obj_dict_.iteritems():
            [ profile.__payer, profile.__balance ] = bkup[prID]
        return rv

    @classmethod
    def _load_(cls, objDump):
        if User._obj_dict_ is None:
            raise ReferenceError("User class need to be loaded first.")
        super(Profile, cls)._load_(objDump)
        # re-build reference table to User instances
        cls._profile_map_ = { }    # re-init the profile mapping
        for profile in cls._obj_dict_.itervalues():
            # re-build ref for payer
            if profile.__payer:
                profile.__payer = User.getObject(profile.__payer)
            else:
                profile.__payer = None
            # re-build ref for balance (for all payees)
            bkup = profile.__balance
            profile.__balance = { }
            for ( uid, blnc ) in bkup.iteritems():
                profile.__balance[User.getObject(uid)] = blnc
            cls._profile_map_[profile.getName()] = profile    # re-build mapping
        return


    ######## Private Member Functions ########

    def __changeName(self, name, new=False):
        if name in Profile._profile_map_:     # check name conflict
            raise ValueError('profile "{0}" already exists in the system'.format(name))

        if not new:                        # remove old name from the mapping
            del Profile._profile_map_[self.__name]
        self.__name = name                 # update the name
        Profile._profile_map_[name] = self # add the new name to the mapping
        if new:
            self.__history = History(self.__name)
        else:
            self.__history.changeFn(name)
        return



########
######## ########
########

import os

class History(object):
    """ payment history class """

    _archive_prefix_ = [ ]      # path list

    # when records exceed the archive limit, the first `archive amount` of
    # them get archived to "archive prefix/fn" file
    _archive_limit_  = -1       # default: never archive
    _archive_amount_ = 0        # default: never archive

    ######## Overridden build-in Methods / Operators ########

    def __init__(self, fn):
        self.__fn = safeFn(fn)
        self.__rec = [ ]
        return


    ######## Public Functions ########

    def changeFn(self, fn):
        # 
        # rename underlaying data file here
        # 
        self.__fn = safeFn(fn)
        return


    ######## Private Member Functions ########
